/*
 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.naming.internal;

import java.io.InputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;
import java.util.WeakHashMap;

import javax.naming.*;

/**
 * The ResourceManager class facilitates the reading of JNDI resource files.
 *
 * @author Rosanna Lee
 * @author Scott Seligman
 */

public final class ResourceManager {

  /*
   * Name of provider resource files (without the package-name prefix.)
   */
  private static final String PROVIDER_RESOURCE_FILE_NAME =
      "jndiprovider.properties";

  /*
   * Name of application resource files.
   */
  private static final String APP_RESOURCE_FILE_NAME = "jndi.properties";

  /*
   * Name of properties file in <java.home>/lib.
   */
  private static final String JRELIB_PROPERTY_FILE_NAME = "jndi.properties";

  /*
   * Internal environment property, that when set to "true", disables
   * application resource files lookup to prevent recursion issues
   * when validating signed JARs.
   */
  private static final String DISABLE_APP_RESOURCE_FILES =
      "com.sun.naming.disable.app.resource.files";

  /*
   * The standard JNDI properties that specify colon-separated lists.
   */
  private static final String[] listProperties = {
      Context.OBJECT_FACTORIES,
      Context.URL_PKG_PREFIXES,
      Context.STATE_FACTORIES,
      // The following shouldn't create a runtime dependence on ldap package.
      javax.naming.ldap.LdapContext.CONTROL_FACTORIES
  };

  private static final VersionHelper helper =
      VersionHelper.getVersionHelper();

  /*
   * A cache of the properties that have been constructed by
   * the ResourceManager.  A Hashtable from a provider resource
   * file is keyed on a class in the resource file's package.
   * One from application resource files is keyed on the thread's
   * context class loader.
   */
  // WeakHashMap<Class | ClassLoader, Hashtable>
  private static final WeakHashMap<Object, Hashtable<? super String, Object>>
      propertiesCache = new WeakHashMap<>(11);

  /*
   * A cache of factory objects (ObjectFactory, StateFactory, ControlFactory).
   *
   * A two-level cache keyed first on context class loader and then
   * on propValue.  Value is a list of class or factory objects,
   * weakly referenced so as not to prevent GC of the class loader.
   * Used in getFactories().
   */
  private static final
  WeakHashMap<ClassLoader, Map<String, List<NamedWeakReference<Object>>>>
      factoryCache = new WeakHashMap<>(11);

  /*
   * A cache of URL factory objects (ObjectFactory).
   *
   * A two-level cache keyed first on context class loader and then
   * on classSuffix+propValue.  Value is the factory itself (weakly
   * referenced so as not to prevent GC of the class loader) or
   * NO_FACTORY if a previous search revealed no factory.  Used in
   * getFactory().
   */
  private static final
  WeakHashMap<ClassLoader, Map<String, WeakReference<Object>>>
      urlFactoryCache = new WeakHashMap<>(11);
  private static final WeakReference<Object> NO_FACTORY =
      new WeakReference<>(null);

  /**
   * A class to allow JNDI properties be specified as applet parameters
   * without creating a static dependency on java.applet.
   */
  private static class AppletParameter {

    private static final Class<?> clazz = getClass("java.applet.Applet");
    private static final Method getMethod =
        getMethod(clazz, "getParameter", String.class);

    private static Class<?> getClass(String name) {
      try {
        return Class.forName(name, true, null);
      } catch (ClassNotFoundException e) {
        return null;
      }
    }

    private static Method getMethod(Class<?> clazz,
        String name,
        Class<?>... paramTypes) {
      if (clazz != null) {
        try {
          return clazz.getMethod(name, paramTypes);
        } catch (NoSuchMethodException e) {
          throw new AssertionError(e);
        }
      } else {
        return null;
      }
    }

    /**
     * Returns the value of the applet's named parameter.
     */
    static Object get(Object applet, String name) {
      // if clazz is null then applet cannot be an Applet.
      if (clazz == null || !clazz.isInstance(applet)) {
        throw new ClassCastException(applet.getClass().getName());
      }
      try {
        return getMethod.invoke(applet, name);
      } catch (InvocationTargetException |
          IllegalAccessException e) {
        throw new AssertionError(e);
      }
    }
  }

  // There should be no instances of this class.
  private ResourceManager() {
  }

  // ---------- Public methods ----------

  /*
   * Given the environment parameter passed to the initial context
   * constructor, returns the full environment for that initial
   * context (never null).  This is based on the environment
   * parameter, the applet parameters (where appropriate), the
   * system properties, and all application resource files.
   *
   * <p> This method will modify <tt>env</tt> and save
   * a reference to it.  The caller may no longer modify it.
   *
   * @param env       environment passed to initial context constructor.
   *                  Null indicates an empty environment.
   *
   * @throws NamingException if an error occurs while reading a
   *          resource file
   */
  @SuppressWarnings("unchecked")
  public static Hashtable<?, ?> getInitialEnvironment(
      Hashtable<?, ?> env)
      throws NamingException {
    String[] props = VersionHelper.PROPS;   // system/applet properties
    if (env == null) {
      env = new Hashtable<>(11);
    }
    Object applet = env.get(Context.APPLET);

    // Merge property values from env param, applet params, and system
    // properties.  The first value wins:  there's no concatenation of
    // colon-separated lists.
    // Read system properties by first trying System.getProperties(),
    // and then trying System.getProperty() if that fails.  The former
    // is more efficient due to fewer permission checks.
    //
    String[] jndiSysProps = helper.getJndiProperties();
    for (int i = 0; i < props.length; i++) {
      Object val = env.get(props[i]);
      if (val == null) {
        if (applet != null) {
          val = AppletParameter.get(applet, props[i]);
        }
        if (val == null) {
          // Read system property.
          val = (jndiSysProps != null)
              ? jndiSysProps[i]
              : helper.getJndiProperty(i);
        }
        if (val != null) {
          ((Hashtable<String, Object>) env).put(props[i], val);
        }
      }
    }

    // Return without merging if application resource files lookup
    // is disabled.
    String disableAppRes = (String) env.get(DISABLE_APP_RESOURCE_FILES);
    if (disableAppRes != null && disableAppRes.equalsIgnoreCase("true")) {
      return env;
    }

    // Merge the above with the values read from all application
    // resource files.  Colon-separated lists are concatenated.
    mergeTables((Hashtable<Object, Object>) env, getApplicationResources());
    return env;
  }

  /**
   * Retrieves the property from the environment, or from the provider
   * resource file associated with the given context.  The environment
   * may in turn contain values that come from applet parameters,
   * system properties, or application resource files.
   *
   * If <tt>concat</tt> is true and both the environment and the provider
   * resource file contain the property, the two values are concatenated
   * (with a ':' separator).
   *
   * Returns null if no value is found.
   *
   * @param propName The non-null property name
   * @param env The possibly null environment properties
   * @param ctx The possibly null context
   * @param concat True if multiple values should be concatenated
   * @return the property value, or null is there is none.
   * @throws NamingException if an error occurs while reading the provider resource file.
   */
  public static String getProperty(String propName, Hashtable<?, ?> env,
      Context ctx, boolean concat)
      throws NamingException {

    String val1 = (env != null) ? (String) env.get(propName) : null;
    if ((ctx == null) ||
        ((val1 != null) && !concat)) {
      return val1;
    }
    String val2 = (String) getProviderResource(ctx).get(propName);
    if (val1 == null) {
      return val2;
    } else if ((val2 == null) || !concat) {
      return val1;
    } else {
      return (val1 + ":" + val2);
    }
  }

  /**
   * Retrieves an enumeration of factory classes/object specified by a
   * property.
   *
   * The property is gotten from the environment and the provider
   * resource file associated with the given context and concantenated.
   * See getProperty(). The resulting property value is a list of class names.
   * <p>
   * This method then loads each class using the current thread's context
   * class loader and keeps them in a list. Any class that cannot be loaded
   * is ignored. The resulting list is then cached in a two-level
   * hash table, keyed first by the context class loader and then by
   * the property's value.
   * The next time threads of the same context class loader call this
   * method, they can use the cached list.
   * <p>
   * After obtaining the list either from the cache or by creating one from
   * the property value, this method then creates and returns a
   * FactoryEnumeration using the list. As the FactoryEnumeration is
   * traversed, the cached Class object in the list is instantiated and
   * replaced by an instance of the factory object itself.  Both class
   * objects and factories are wrapped in weak references so as not to
   * prevent GC of the class loader.
   * <p>
   * Note that multiple threads can be accessing the same cached list
   * via FactoryEnumeration, which locks the list during each next().
   * The size of the list will not change,
   * but a cached Class object might be replaced by an instantiated factory
   * object.
   *
   * @param propName The non-null property name
   * @param env The possibly null environment properties
   * @param ctx The possibly null context
   * @return An enumeration of factory classes/objects; null if none.
   * @throws NamingException If encounter problem while reading the provider property file.
   * @see javax.naming.spi.NamingManager#getObjectInstance
   * @see javax.naming.spi.NamingManager#getStateToBind
   * @see javax.naming.spi.DirectoryManager#getObjectInstance
   * @see javax.naming.spi.DirectoryManager#getStateToBind
   * @see javax.naming.ldap.ControlFactory#getControlInstance
   */
  public static FactoryEnumeration getFactories(String propName,
      Hashtable<?, ?> env, Context ctx) throws NamingException {

    String facProp = getProperty(propName, env, ctx, true);
    if (facProp == null) {
      return null;  // no classes specified; return null
    }

    // Cache is based on context class loader and property val
    ClassLoader loader = helper.getContextClassLoader();

    Map<String, List<NamedWeakReference<Object>>> perLoaderCache = null;
    synchronized (factoryCache) {
      perLoaderCache = factoryCache.get(loader);
      if (perLoaderCache == null) {
        perLoaderCache = new HashMap<>(11);
        factoryCache.put(loader, perLoaderCache);
      }
    }

    synchronized (perLoaderCache) {
      List<NamedWeakReference<Object>> factories =
          perLoaderCache.get(facProp);
      if (factories != null) {
        // Cached list
        return factories.size() == 0 ? null
            : new FactoryEnumeration(factories, loader);
      } else {
        // Populate list with classes named in facProp; skipping
        // those that we cannot load
        StringTokenizer parser = new StringTokenizer(facProp, ":");
        factories = new ArrayList<>(5);
        while (parser.hasMoreTokens()) {
          try {
            // System.out.println("loading");
            String className = parser.nextToken();
            Class<?> c = helper.loadClass(className, loader);
            factories.add(new NamedWeakReference<Object>(c, className));
          } catch (Exception e) {
            // ignore ClassNotFoundException, IllegalArgumentException
          }
        }
        // System.out.println("adding to cache: " + factories);
        perLoaderCache.put(facProp, factories);
        return new FactoryEnumeration(factories, loader);
      }
    }
  }

  /**
   * Retrieves a factory from a list of packages specified in a
   * property.
   *
   * The property is gotten from the environment and the provider
   * resource file associated with the given context and concatenated.
   * classSuffix is added to the end of this list.
   * See getProperty(). The resulting property value is a list of package
   * prefixes.
   * <p>
   * This method then constructs a list of class names by concatenating
   * each package prefix with classSuffix and attempts to load and
   * instantiate the class until one succeeds.
   * Any class that cannot be loaded is ignored.
   * The resulting object is then cached in a two-level hash table,
   * keyed first by the context class loader and then by the property's
   * value and classSuffix.
   * The next time threads of the same context class loader call this
   * method, they use the cached factory.
   * If no factory can be loaded, NO_FACTORY is recorded in the table
   * so that next time it'll return quickly.
   *
   * @param propName The non-null property name
   * @param env The possibly null environment properties
   * @param ctx The possibly null context
   * @param classSuffix The non-null class name (e.g. ".ldap.ldapURLContextFactory).
   * @param defaultPkgPrefix The non-null default package prefix. (e.g., "com.sun.jndi.url").
   * @return An factory object; null if none.
   * @throws NamingException If encounter problem while reading the provider property file, or
   * problem instantiating the factory.
   * @see javax.naming.spi.NamingManager#getURLContext
   * @see javax.naming.spi.NamingManager#getURLObject
   */
  public static Object getFactory(String propName, Hashtable<?, ?> env,
      Context ctx, String classSuffix, String defaultPkgPrefix)
      throws NamingException {

    // Merge property with provider property and supplied default
    String facProp = getProperty(propName, env, ctx, true);
    if (facProp != null) {
      facProp += (":" + defaultPkgPrefix);
    } else {
      facProp = defaultPkgPrefix;
    }

    // Cache factory based on context class loader, class name, and
    // property val
    ClassLoader loader = helper.getContextClassLoader();
    String key = classSuffix + " " + facProp;

    Map<String, WeakReference<Object>> perLoaderCache = null;
    synchronized (urlFactoryCache) {
      perLoaderCache = urlFactoryCache.get(loader);
      if (perLoaderCache == null) {
        perLoaderCache = new HashMap<>(11);
        urlFactoryCache.put(loader, perLoaderCache);
      }
    }

    synchronized (perLoaderCache) {
      Object factory = null;

      WeakReference<Object> factoryRef = perLoaderCache.get(key);
      if (factoryRef == NO_FACTORY) {
        return null;
      } else if (factoryRef != null) {
        factory = factoryRef.get();
        if (factory != null) {  // check if weak ref has been cleared
          return factory;
        }
      }

      // Not cached; find first factory and cache
      StringTokenizer parser = new StringTokenizer(facProp, ":");
      String className;
      while (factory == null && parser.hasMoreTokens()) {
        className = parser.nextToken() + classSuffix;
        try {
          // System.out.println("loading " + className);
          factory = helper.loadClass(className, loader).newInstance();
        } catch (InstantiationException e) {
          NamingException ne =
              new NamingException("Cannot instantiate " + className);
          ne.setRootCause(e);
          throw ne;
        } catch (IllegalAccessException e) {
          NamingException ne =
              new NamingException("Cannot access " + className);
          ne.setRootCause(e);
          throw ne;
        } catch (Exception e) {
          // ignore ClassNotFoundException, IllegalArgumentException,
          // etc.
        }
      }

      // Cache it.
      perLoaderCache.put(key, (factory != null)
          ? new WeakReference<>(factory)
          : NO_FACTORY);
      return factory;
    }
  }

  // ---------- Private methods ----------

  /*
   * Returns the properties contained in the provider resource file
   * of an object's package.  Returns an empty hash table if the
   * object is null or the resource file cannot be found.  The
   * results are cached.
   *
   * @throws NamingException if an error occurs while reading the file.
   */
  private static Hashtable<? super String, Object>
  getProviderResource(Object obj)
      throws NamingException {
    if (obj == null) {
      return (new Hashtable<>(1));
    }
    synchronized (propertiesCache) {
      Class<?> c = obj.getClass();

      Hashtable<? super String, Object> props =
          propertiesCache.get(c);
      if (props != null) {
        return props;
      }
      props = new Properties();

      InputStream istream =
          helper.getResourceAsStream(c, PROVIDER_RESOURCE_FILE_NAME);

      if (istream != null) {
        try {
          ((Properties) props).load(istream);
        } catch (IOException e) {
          NamingException ne = new ConfigurationException(
              "Error reading provider resource file for " + c);
          ne.setRootCause(e);
          throw ne;
        }
      }
      propertiesCache.put(c, props);
      return props;
    }
  }


  /*
   * Returns the Hashtable (never null) that results from merging
   * all application resource files available to this thread's
   * context class loader.  The properties file in <java.home>/lib
   * is also merged in.  The results are cached.
   *
   * SECURITY NOTES:
   * 1.  JNDI needs permission to read the application resource files.
   * 2.  Any class will be able to use JNDI to view the contents of
   * the application resource files in its own classpath.  Give
   * careful consideration to this before storing sensitive
   * information there.
   *
   * @throws NamingException if an error occurs while reading a resource
   *  file.
   */
  private static Hashtable<? super String, Object> getApplicationResources()
      throws NamingException {

    ClassLoader cl = helper.getContextClassLoader();

    synchronized (propertiesCache) {
      Hashtable<? super String, Object> result = propertiesCache.get(cl);
      if (result != null) {
        return result;
      }

      try {
        NamingEnumeration<InputStream> resources =
            helper.getResources(cl, APP_RESOURCE_FILE_NAME);
        try {
          while (resources.hasMore()) {
            Properties props = new Properties();
            InputStream istream = resources.next();
            try {
              props.load(istream);
            } finally {
              istream.close();
            }

            if (result == null) {
              result = props;
            } else {
              mergeTables(result, props);
            }
          }
        } finally {
          while (resources.hasMore()) {
            resources.next().close();
          }
        }

        // Merge in properties from file in <java.home>/lib.
        InputStream istream =
            helper.getJavaHomeLibStream(JRELIB_PROPERTY_FILE_NAME);
        if (istream != null) {
          try {
            Properties props = new Properties();
            props.load(istream);

            if (result == null) {
              result = props;
            } else {
              mergeTables(result, props);
            }
          } finally {
            istream.close();
          }
        }

      } catch (IOException e) {
        NamingException ne = new ConfigurationException(
            "Error reading application resource file");
        ne.setRootCause(e);
        throw ne;
      }
      if (result == null) {
        result = new Hashtable<>(11);
      }
      propertiesCache.put(cl, result);
      return result;
    }
  }

  /*
   * Merge the properties from one hash table into another.  Each
   * property in props2 that is not in props1 is added to props1.
   * For each property in both hash tables that is one of the
   * standard JNDI properties that specify colon-separated lists,
   * the values are concatenated and stored in props1.
   */
  private static void mergeTables(Hashtable<? super String, Object> props1,
      Hashtable<? super String, Object> props2) {
    for (Object key : props2.keySet()) {
      String prop = (String) key;
      Object val1 = props1.get(prop);
      if (val1 == null) {
        props1.put(prop, props2.get(prop));
      } else if (isListProperty(prop)) {
        String val2 = (String) props2.get(prop);
        props1.put(prop, ((String) val1) + ":" + val2);
      }
    }
  }

  /*
   * Is a property one of the standard JNDI properties that specify
   * colon-separated lists?
   */
  private static boolean isListProperty(String prop) {
    prop = prop.intern();
    for (int i = 0; i < listProperties.length; i++) {
      if (prop == listProperties[i]) {
        return true;
      }
    }
    return false;
  }
}
